using System;
using Microsoft.Xna.Framework;
using Phoenix.Xna.Framework.Graphics.Models;
using Phoenix.Xna.Framework.Properties;

namespace Phoenix.Xna.Framework.Graphics.Animation
{
    public class AnimationController : IAnimationController
    {
        private SkinnedModelBoneCollection _skeleton;
        private float _speed = 1.0f;
        private bool _isLooping = true;
        private bool _crossFadeEnabled = false;
        private AnimationClip _crossFadeAnimationClip;
        private float _crossFadeInterpolationAmount = 0.0f;
        private TimeSpan _crossFadeTime = TimeSpan.Zero;
        private TimeSpan _crossFadeElapsedTime = TimeSpan.Zero;
        private InterpolationMode _crossFadeTranslationInterpolation;
        private InterpolationMode _crossFadeOrientationInterpolation;
        private InterpolationMode _crossFadeScaleInterpolation;

        public AnimationController(SkinnedModelBoneCollection skeleton)
        {
            _skeleton = skeleton;
            LocalBonePoses = new Pose[skeleton.Count];
            SkinnedBoneTransforms = new Matrix[skeleton.Count];
            skeleton[0].CopyBindPoseTo(LocalBonePoses);
            Time = TimeSpan.Zero;
            PlaybackMode = PlaybackMode.Forward;
            BlendWeight = 1.0f;
            TranslationInterpolation = InterpolationMode.None;
            OrientationInterpolation = InterpolationMode.None;
            ScaleInterpolation = InterpolationMode.None;
            IsFinished = false;
            IsPlaying = false;
        }

        private void UpdateCrossFadeTime(TimeSpan elapsedTime)
        {
            _crossFadeElapsedTime += elapsedTime;

            if (_crossFadeElapsedTime > _crossFadeTime)
            {
                _crossFadeEnabled = false;
                _crossFadeInterpolationAmount = 0;
                _crossFadeTime = TimeSpan.Zero;
                _crossFadeElapsedTime = TimeSpan.Zero;
                StartClip(_crossFadeAnimationClip);
            }
            else
            {
                _crossFadeInterpolationAmount = _crossFadeElapsedTime.Ticks / (float)_crossFadeTime.Ticks;
            }
        }

        private void UpdateAnimationTime(TimeSpan elapsedTime)
        {
            if (PlaybackMode == PlaybackMode.Forward)
            {
                Time += elapsedTime;
            }
            else
            {
                Time -= elapsedTime;
            }

            if (Time < TimeSpan.Zero || Time > AnimationClip.Duration)
            {
                if (_isLooping)
                {
                    if (Time > AnimationClip.Duration)
                    {
                        while (Time > AnimationClip.Duration)
                        {
                            Time -= AnimationClip.Duration;
                        }
                    }
                    else
                    {
                        while (Time < TimeSpan.Zero)
                        {
                            Time += AnimationClip.Duration;
                        }
                    }

                    _skeleton[0].CopyBindPoseTo(LocalBonePoses);
                }
                else
                {
                    if (Time > AnimationClip.Duration)
                    {
                        Time = AnimationClip.Duration;
                    }
                    else
                    {
                        Time = TimeSpan.Zero;
                    }

                    IsPlaying = false;
                    IsFinished = true;
                }
            }
        }

        private void UpdateChannelPoses()
        {
            AnimationChannel animationChannel;
            Pose channelPose;

            for (int i = 0; i < LocalBonePoses.Length; i++)
            {
                string animationChannelName = _skeleton[i].Name;

                if (AnimationClip.Channels.TryGetValue(animationChannelName, out animationChannel))
                {
                    InterpolateChannelPose(animationChannel, Time, out channelPose);
                    LocalBonePoses[i] = channelPose;
                }

                if (_crossFadeEnabled)
                {
                    if (_crossFadeAnimationClip.Channels.TryGetValue(animationChannelName, out animationChannel))
                    {
                        InterpolateChannelPose(animationChannel, TimeSpan.Zero, out channelPose);
                    }
                    else
                    {
                        channelPose = _skeleton[i].BindPose;
                    }

                    LocalBonePoses[i] = Pose.Interpolate(
                        LocalBonePoses[i],
                        channelPose,
                        _crossFadeInterpolationAmount,
                        _crossFadeTranslationInterpolation,
                        _crossFadeOrientationInterpolation,
                        _crossFadeScaleInterpolation);
                }
            }
        }

        private void InterpolateChannelPose(AnimationChannel animationChannel, TimeSpan animationTime, out Pose outPose)
        {
            if (TranslationInterpolation == InterpolationMode.None && OrientationInterpolation == InterpolationMode.None && ScaleInterpolation == InterpolationMode.None)
            {
                int keyframeIndex = animationChannel.GetKeyframeIndexByTime(animationTime);
                outPose = animationChannel[keyframeIndex].Pose;
            }
            else
            {
                int keyframeIndex = animationChannel.GetKeyframeIndexByTime(animationTime);
                int nextKeyframeIndex = (keyframeIndex + 1) % animationChannel.Count;
                AnimationChannelKeyframe keyframe1 = animationChannel[keyframeIndex];
                AnimationChannelKeyframe keyframe2 = animationChannel[nextKeyframeIndex];
                long keyframeDuration;

                if (keyframeIndex == (animationChannel.Count - 1))
                {
                    keyframeDuration = AnimationClip.Duration.Ticks - keyframe1.Time.Ticks;
                }
                else
                {
                    keyframeDuration = keyframe2.Time.Ticks - keyframe1.Time.Ticks;
                }

                if (keyframeDuration > 0)
                {
                    long elapsedKeyframeTime = animationTime.Ticks - keyframe1.Time.Ticks;
                    float lerpFactor = elapsedKeyframeTime / (float)keyframeDuration;
                    outPose = Pose.Interpolate(keyframe1.Pose, keyframe2.Pose, lerpFactor, TranslationInterpolation, OrientationInterpolation, ScaleInterpolation);
                }
                else
                {
                    outPose = keyframe1.Pose;
                }
            }
        }

        private void UpdateAbsoluteBoneTransforms(ref Matrix parent)
        {
            Matrix poseTransform;
            poseTransform = Matrix.CreateFromQuaternion(LocalBonePoses[0].Orientation);
            poseTransform.Translation = LocalBonePoses[0].Translation;
            poseTransform.M11 *= LocalBonePoses[0].Scale.X;
            poseTransform.M21 *= LocalBonePoses[0].Scale.X;
            poseTransform.M31 *= LocalBonePoses[0].Scale.X;
            poseTransform.M12 *= LocalBonePoses[0].Scale.Y;
            poseTransform.M22 *= LocalBonePoses[0].Scale.Y;
            poseTransform.M32 *= LocalBonePoses[0].Scale.Y;
            poseTransform.M13 *= LocalBonePoses[0].Scale.Z;
            poseTransform.M23 *= LocalBonePoses[0].Scale.Z;
            poseTransform.M33 *= LocalBonePoses[0].Scale.Z;
            SkinnedBoneTransforms[0] = poseTransform * parent;

            for (int i = 1; i < SkinnedBoneTransforms.Length; i++)
            {
                poseTransform = Matrix.CreateFromQuaternion(LocalBonePoses[i].Orientation);
                poseTransform.Translation = LocalBonePoses[i].Translation;
                poseTransform.M11 *= LocalBonePoses[i].Scale.X;
                poseTransform.M21 *= LocalBonePoses[i].Scale.X;
                poseTransform.M31 *= LocalBonePoses[i].Scale.X;
                poseTransform.M12 *= LocalBonePoses[i].Scale.Y;
                poseTransform.M22 *= LocalBonePoses[i].Scale.Y;
                poseTransform.M32 *= LocalBonePoses[i].Scale.Y;
                poseTransform.M13 *= LocalBonePoses[i].Scale.Z;
                poseTransform.M23 *= LocalBonePoses[i].Scale.Z;
                poseTransform.M33 *= LocalBonePoses[i].Scale.Z;
                int parentIndex = _skeleton[i].Parent.Index;
                SkinnedBoneTransforms[i] = poseTransform * SkinnedBoneTransforms[parentIndex];
            }

            for (int i = 0; i < SkinnedBoneTransforms.Length; i++)
            {
                SkinnedBoneTransforms[i] = _skeleton[i].InverseBindPoseTransform * SkinnedBoneTransforms[i];
            }
        }

        #region IAnimationController Members

        public AnimationClip AnimationClip { get; private set; }
        public TimeSpan Time { get; set; }
        public float Speed
        {
            get { return _speed; }
            set
            {
                if (_speed < 0)
                {
                    throw new ArgumentException(string.Format(Resources.ArgumentOutOfRangeException, "_speed", 0, "infinity"));
                }

                _speed = value;
            }
        }
        public bool IsLooping
        {
            get { return _isLooping; }
            set
            {
                _isLooping = value;

                if (IsFinished && _isLooping)
                {
                    IsFinished = false;
                }
            }
        }
        public PlaybackMode PlaybackMode { get; set; }
        public InterpolationMode TranslationInterpolation { get; set; }
        public InterpolationMode OrientationInterpolation { get; set; }
        public InterpolationMode ScaleInterpolation { get; set; }
        public bool IsFinished { get; private set; }
        public bool IsPlaying { get; private set; }
        public Matrix[] SkinnedBoneTransforms { get; private set; }

        public void StartClip(AnimationClip animationClip)
        {
            AnimationClip = animationClip;
            IsFinished = false;
            IsPlaying = true;
            Time = TimeSpan.Zero;
            _skeleton[0].CopyBindPoseTo(LocalBonePoses);
        }

        public void PlayClip(AnimationClip animationClip)
        {
            AnimationClip = animationClip;

            if (Time < animationClip.Duration)
            {
                IsFinished = false;
                IsPlaying = true;
            }
        }

        public void CrossFade(AnimationClip animationClip, TimeSpan fadeTime)
        {
            CrossFade(animationClip, fadeTime, InterpolationMode.Linear, InterpolationMode.Linear, InterpolationMode.Linear);
        }

        public void CrossFade(AnimationClip animationClip, TimeSpan fadeTime, InterpolationMode translationInterpolation,
            InterpolationMode orientationInterpolation, InterpolationMode scaleInterpolation)
        {
            if (_crossFadeEnabled)
            {
                _crossFadeInterpolationAmount = 0;
                _crossFadeTime = TimeSpan.Zero;
                _crossFadeElapsedTime = TimeSpan.Zero;
                StartClip(_crossFadeAnimationClip);
            }

            _crossFadeAnimationClip = animationClip;
            _crossFadeTime = fadeTime;
            _crossFadeElapsedTime = TimeSpan.Zero;
            _crossFadeTranslationInterpolation = translationInterpolation;
            _crossFadeOrientationInterpolation = orientationInterpolation;
            _crossFadeScaleInterpolation = scaleInterpolation;
            _crossFadeEnabled = true;
        }

        public void Update(TimeSpan elapsedTime, Matrix parent)
        {
            if (IsFinished)
            {
                return;
            }

            TimeSpan scaledElapsedTime = TimeSpan.FromTicks((long)(elapsedTime.Ticks * _speed));

            if (AnimationClip != null)
            {
                UpdateAnimationTime(scaledElapsedTime);

                if (_crossFadeEnabled)
                {
                    UpdateCrossFadeTime(scaledElapsedTime);
                }

                UpdateChannelPoses();
            }

            UpdateAbsoluteBoneTransforms(ref parent);
        }

        #endregion

        #region IBlendable Members

        public Pose[] LocalBonePoses { get; private set; }
        public float BlendWeight { get; set; }

        #endregion
    }
}